﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Practices.Unity;
using MiniStock.Entities.Common.Events;
using MiniStock.Infrastructure;
using System.Diagnostics;

namespace MiniStock.BusinessLogic.EventDispatcher.MessageMap
{
    public class MessageMapHandler
    {
        [Dependency]
        public IRepository<MessageMapRepositoryEntry> Repository { get; set; }

        public static readonly ReaderWriterLock MessageMapLock = new ReaderWriterLock();
        public static readonly MessageMap MessageMap = new MessageMap();
        public static readonly object MessageCounterLock = new object();
        public static int MessageCounter = 0;

        public void AddMessage(EventBase message)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            if (message.EventId == Guid.Empty)
                throw new ArgumentException("EventId must be specified", "message");

            if (message.CorrelatedEventId == null)
                throw new ArgumentException("CorrelatedEventId must be specified", "message");

            MessageMapEntry entry = GetEntry(message.CorrelatedEventId.Value, true);

            try
            {
                Trace.TraceInformation("Acquire lock for tid: {0} cid: {1}", 
                    Thread.CurrentThread.ManagedThreadId,
                    message.CorrelatedEventId);
                entry.Lock.AcquireWriterLock(100);
                entry.Events.Add(message);
                entry.ModificationDate = DateTime.UtcNow;
                if (entry.Events.Count == 1)
                    entry.Status = MessageMapStatus.Submitted;
                else if (message is MessageProcessedEvent)
                    entry.Status = MessageMapStatus.Processed;
                else if (message is MessagePersistedEvent)
                    entry.Status = MessageMapStatus.Persisted;
                else if (message is MessageFaultedEvent)
                    entry.Status = MessageMapStatus.Faulted;
            }
            catch (Exception ex)
            {
                Trace.TraceWarning("Error adding event to message map for cid: {0}: {1}", message.CorrelatedEventId, ex);
                throw;
            }
            finally
            {
                Trace.TraceInformation("Release lock for tid: {0} cid: {1}",
                    Thread.CurrentThread.ManagedThreadId,
                    message.CorrelatedEventId);
                entry.Lock.ReleaseLock();
            }

            Repository.Add(new MessageMapRepositoryEntry()
                               {
                                   Id = Guid.NewGuid(),
                                   CorrelationId = message.CorrelatedEventId.Value,
                                   Timestamp = DateTime.UtcNow,
                                   Message = message,
                               });

            lock(MessageCounterLock)
            {
                MessageCounter--;
                if(MessageCounter <= 0)
                {
                    Repository.Add(new MessageMapRepositoryEntry()
                                       {
                                           Id = Guid.NewGuid(),
                                           CorrelationId = Guid.NewGuid(),
                                           Timestamp = DateTime.UtcNow,
                                           Snapshot = MessageMap,
                                       });
                    MessageCounter = 100;
                }
            }

            if(Repository.BatchCommit)
                Repository.Commit();

        }

        public void SetMessagePersisted(EventBase message)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            if (message.EventId == Guid.Empty)
                throw new ArgumentException("EventId must be specified", "message");

            if (message.CorrelatedEventId == null)
                throw new ArgumentException("CorrelatedEventId must be specified", "message");

            MessageMapEntry entry = GetEntry(message.CorrelatedEventId.Value, false);

            try
            {
                Trace.TraceInformation("Acquire lock for tid: {0} cid: {1}",
                    Thread.CurrentThread.ManagedThreadId,
                    message.CorrelatedEventId);

                entry.Lock.AcquireWriterLock(100);
                entry.ModificationDate = DateTime.UtcNow;
                if (entry.Status != MessageMapStatus.Completed)
                    entry.Status = MessageMapStatus.Persisted;
            }
            catch (Exception ex)
            {
                Trace.TraceWarning("Error setting message map state for cid: {0}: {1}", message.CorrelatedEventId, ex);
                throw;
            }
            finally
            {
                Trace.TraceInformation("Release lock for tid: {0} cid: {1}",
                    Thread.CurrentThread.ManagedThreadId,
                    message.CorrelatedEventId);
                
                entry.Lock.ReleaseLock();
            }
        }

        private static MessageMapEntry GetEntry(Guid id, bool create = false)
        {
            MessageMapEntry entry = null;
            try
            {
                Trace.TraceInformation("Acquire reader lock for tid: {0} cid: {1}",
                                          Thread.CurrentThread.ManagedThreadId,
                                          id);
                MessageMapLock.AcquireReaderLock(1000);

                if (!MessageMap.TryGetValue(id, out entry) && create)
                {
                    Trace.TraceInformation("Upgrade lock for tid: {0} cid: {1}",
                                              Thread.CurrentThread.ManagedThreadId,
                                              id);
                    MessageMapLock.UpgradeToWriterLock(1000);
                    if (!MessageMap.TryGetValue(id, out entry))
                    {
                        entry = new MessageMapEntry() { Status = MessageMapStatus.Created, };
                        MessageMap[id] = entry;
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.TraceWarning("Error getting message map entry for cid: {0}: {1}", id, ex);
                throw;
            }
            finally
            {
                Trace.TraceInformation("Release lock for tid: {0} cid: {1}",
                                          Thread.CurrentThread.ManagedThreadId,
                                          id);
                MessageMapLock.ReleaseLock();
            }

            Trace.TraceInformation("MessageMapHandler.GetEntry CorrelationId={0}, result={1}", id, (entry == null ? (MessageMapStatus?)null : entry.Status));

            return entry;
        }

        public static List<Metric> CollectMetrics()
        {
            var items =
                (from m in MessageMap.ToList()
                 select new {Id = m.Key, Status = m.Value.Status,}
                ).ToList();
            List<Metric> result =
                (from i in Enum.GetNames(typeof (MessageMapStatus))
                 select new Metric()
                            {
                                Value = 0,
                                Name = i.ToString(),
                            }).ToList();
                
            items.ForEach(i =>
                              {
                                  var m = result.First(j => j.Name == i.Status.ToString());
                                  m.Value++;
                              });

            result.ForEach(i => { i.Name = "MessageMap state: " + i.Name; });
            return result;
        }

        public StatusQueryEvent GetResult(Guid correlationId, bool p)
        {
            var result = new StatusQueryEvent()
                                     {
                                         EventId = Guid.NewGuid(),
                                         CorrelatedEventId = correlationId,
                                         EventDate = DateTime.UtcNow,
                                     };

            var entry = GetEntry(correlationId);
            if (entry == null)
            {
                // TODO: check for messages that are persisted out
                result.State = EventProcessingState.Unknown;
            }
            else
            {
                try
                {
                    Trace.TraceInformation("Acquire lock for tid: {0} cid: {1}",
                                              Thread.CurrentThread.ManagedThreadId,
                                              correlationId);
                    entry.Lock.AcquireWriterLock(100);
                    entry.ModificationDate = DateTime.UtcNow;
                    var firstMessage = entry.Events.FirstOrDefault();
                    result.EventType = firstMessage.GetType().Name;
                    var lastResult = entry.Events.LastOrDefault(i => i is ResultEvent) as ResultEvent;
                    var entity = (lastResult != null ? lastResult.Entity : null);

                    Trace.TraceInformation("Status for entry (cid: {0}): {1}, events: {2}", 
                        correlationId, entry.Status, entry.Events.Count());

                    switch (entry.Status)
                    {
                        case MessageMapStatus.Completed:
                            result.State = EventProcessingState.Completed;
                            result.EntityId = entity.Id;
                            result.EntityHash = lastResult.GetEntityHash();
                            break;
                        case MessageMapStatus.Persisted:
                            entry.Status = MessageMapStatus.Completed;
                            result.State = EventProcessingState.Completed;
                            result.EntityId = entity.Id;
                            result.EntityHash = lastResult.GetEntityHash();
                            break;
                        case MessageMapStatus.Faulted:
                            entry.Status = MessageMapStatus.Completed;
                            result.State = EventProcessingState.Faulted;
                            result.Message = ((MessageFaultedEvent) lastResult).ErrorMessage;
                            break;
                        default:
                            result.State = EventProcessingState.Processing;
                            break;
                    }
                }
                catch (Exception ex)
                {
                    Trace.TraceWarning("Error querying message map state for cid: {0}: {1}", correlationId, ex);
                    throw;
                }
                finally
                {
                    Trace.TraceInformation("Release lock for tid: {0} cid: {1}",
                                              Thread.CurrentThread.ManagedThreadId,
                                              correlationId);
                    entry.Lock.ReleaseLock();
                }
            }
            return result;
        }
    }
}
